
package vivace.view;

import java.util.Observable;
import java.util.Observer;
import java.util.Vector;
import java.util.Iterator;
import javax.sound.midi.*;
import vivace.helper.GridHelper;
import vivace.helper.ProjectHelper;
import vivace.model.*;
import javax.swing.JPanel;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseEvent;

public class TempoPanel extends JPanel implements Observer {

	private int maxTempo = 0;
	private int[][] coords;
	private int[] bpms;
	private int height;
	private Vector<MidiEvent> tc;

	/**
	 * Constructor. This is the constructor.
	 */

	TempoPanel () {
		App.addProjectObserver(this,App.Source.ALL);
		this.setBackground(Color.decode("#f5f7f7"));
		this.setPreferredSize( new Dimension(GridHelper.getPianoRollDimension().width, 100) );
		this.height = 100;
		this.coords = recalculateCoords();
		this.addMouseListener(new TempoMouseListener());
		editTime = false;
		currentEditLine = new Point[2];
		currentEditLine[0] = new Point();
		currentEditLine[1] = new Point();
		this.p = new Point();
		this.p.setLocation(60, 50);
		currentEditLine[0].setLocation(coords[0][0], coords[0][1]);
		if (coords.length > 1)
			currentEditLine[1].setLocation(coords[1][0], coords[0][1]);
		else 
			currentEditLine[1].setLocation(coords[0][1],GridHelper.getPianoRollDimension().width);
		this.addMouseMotionListener(new TempoMouseMotionListener());
	}

	public void update (Observable o, Object arg) {
		Action action = (Action)arg;		
		switch (action) {
			case ZOOMLEVEL_CHANGED:
				this.setPreferredSize( new Dimension(GridHelper.getPianoRollDimension().width, this.height) );
				this.coords = recalculateCoords();
			break;
			case PERSPECTIVE_CHANGED:
				this.setPreferredSize( new Dimension(GridHelper.getPianoRollDimension().width, this.height) );
				this.coords = recalculateCoords();
			break;
		}
	}
	
	/*
	* 	Calculate clocks per quarter note from midievent
	* 	@param event
	*/

	private double getCPQN(MidiEvent event) {
		return (ProjectHelper.byteToInt(event.getMessage().getMessage()[3]))*256*256 + (ProjectHelper.byteToInt(event.getMessage().getMessage()[4]))*256 + (ProjectHelper.byteToInt(event.getMessage().getMessage()[5]));
	}

	/* 
 	* 	Convert bpm to clocks per quarter note
 	* 	@param bpm 
	*/

	private double bpmToCPQN(double bpm) {
	return 60000000/bpm;
	}

	/*
	* 	Recalculate the coordinates of the tempo curve
	* 	It also sets all the bpms in the bpms[] field
	* 	@returns a matrix of all the coordinates in the tempocurve
	**/ 
	
	public int[][] recalculateCoords() {
		int bpm,i2 = 0;
		int[][] coord;
		MidiEvent current;
		tc = ProjectHelper.filterMetaMessages(App.Project.getTrack(0), MetaMessageType.TEMPO_CHANGE);
		if (tc.size() == 0) { // if there are no tempochanges in the song, set the coordinates to the beginning and end of the curve
			coord = new int[2][2];
			coord[0][0] = 0;
			coord[0][1] = height/2;
			coord[1][0] = GridHelper.getPianoRollDimension().width;
			coord[1][1] = height/2;
			this.bpms = new int[1];
			bpms[0] = 120; // and set the only bpm to the default value 120 bpm
			return coord;
		}
		if (tc.size() > 1) {
			coord = new int[tc.size() + 1][2];
			this.bpms = new int[tc.size()];
			for (Iterator<MidiEvent> iter = tc.iterator();iter.hasNext();) {
				current = (MidiEvent)iter.next();
				bpm =  (int)(60000000/getCPQN(current));
				if (maxTempo < bpm)
					maxTempo = bpm;
			}
			coord[tc.size()][0] = GridHelper.getPianoRollDimension().width;
			coord[tc.size()][1] = coord[tc.size() - 1][1];
		} else {
			coord = new int[2][2];
			this.bpms = new int[1];
			current = (MidiEvent)tc.firstElement();
			maxTempo = 2*(int)(60000000/getCPQN(current));
			coord[1][0] = GridHelper.getPianoRollDimension().width;
		}
		
		for (Iterator<MidiEvent> iter = tc.iterator();iter.hasNext();) {
			current = (MidiEvent)iter.next();
			bpm =  (int)(60000000/getCPQN(current)); //(1/(mp4b * 0.000001 / 60));
			bpms[i2] = bpm;
			coord[i2][0] = GridHelper.tickToXPosition(current.getTick());
			coord[i2][1] = (int)(this.height - ((double)bpm/maxTempo*this.height));
			i2++;
		}
		return coord;
	}
	
	public void paintComponent(Graphics g){
		super.paintComponent(g);
		GridHelper.paintHistogramGrid(g, this.getHeight());

		if (this.height != this.getHeight()) {
			coords = recalculateCoords();
			currentEditLine = findClosestLine(p);
			this.height = this.getHeight();
		}
		for (int index = 0;index < coords.length;index++) { // this loop draws the "helplines" that 
			g.setColor(Color.decode("#969da2"));
			g.drawLine(coords[index][0], 0, coords[index][0], this.getHeight());
			g.setColor(Color.decode("#000000"));
		}
		for (int index = 0;index < coords.length;index++) {
			if((index == 0 && (coords.length == 1)) || (index == coords.length - 1)) 
				g.drawLine(coords[index][0], coords[index][1], GridHelper.getPianoRollDimension().width, coords[index][1]);
			else {
				g.drawLine(coords[index][0], coords[index][1], coords[index+1][0], coords[index][1]);
				g.drawLine(coords[index + 1][0],coords[index][1],coords[index + 1][0],coords[index + 1][1]);
			}
		}
		g.setFont(new Font("Verdana", Font.TRUETYPE_FONT, 10));
		if (editTime)
			g.drawString("t:" + (double)GridHelper.xPositionToTick(currentEditLine[0].x)/App.Project.getPPQ() + " s", currentEditLine[0].x, p.y);
		else {
			if (isEditing)
				g.drawString("bpm:" + pointsToBpm(currentEditLine[1].y), p.x, currentEditLine[1].y + 10);
			else 
				g.drawString("bpm:" + bpms[currentEvent], p.x, currentEditLine[1].y + 10);
		}
		g.setColor(Color.decode("#ff5577"));
		g.drawLine(currentEditLine[0].x,currentEditLine[0].y,currentEditLine[1].x,currentEditLine[1].y);
	}

	/*
	* 	This funktion returns the line that is closest to the point it is given
	* 	It returns two points. The endpoints of the closest line
	* 	@param point
	* 	@returns line
	*/

	private Point[] findClosestLine(Point p) {
		Point[] line = new Point[2];
		line[0] = new Point();
		line[1] = new Point();
		int index = 1;
		while(p.x > coords[index][0] && index < coords.length - 1) //this while-loop finds the first vertical line right of point p
			index++;
		if (Math.abs(p.x - coords[index][0]) < Math.abs(p.x - coords[index - 1][0])) // this tests which of the vertical lines are closest
			if (Math.abs(p.x - coords[index][0]) > Math.abs(p.y - coords[index - 1][1])) { // this compares the closest vertical line 
    				line[0].setLocation(coords[index][0], coords[index - 1][1]);           // with the closest horisontal line
				line[1].setLocation(coords[index - 1][0], coords[index - 1][1]);
				editTime = false;
				currentEvent = index - 1;
			} else {
				line[0].setLocation(coords[index][0], coords[index][1]);
				line[1].setLocation(coords[index][0], coords[index - 1][1]);
				editTime = true;
				currentEvent = index;
			}
		else 
			if (Math.abs(p.x - coords[index - 1][0]) > Math.abs(p.y - coords[index - 1][1])) { // this compares the same as above
				line[0].setLocation(coords[index][0], coords[index - 1][1]);               // but with the vertical line on the other side of the point
				line[1].setLocation(coords[index - 1][0], coords[index - 1][1]);
				editTime = false;
				currentEvent = index - 1;
			} else if (index != 1){ // if the point is closest to the left end of the panel, then use the last alternative
				line[0].setLocation(coords[index - 1][0], coords[index - 1][1]);
				line[1].setLocation(coords[index - 1][0], coords[index - 2][1]);
				editTime = true;
				currentEvent = index - 1;
			} else {
				line[0].setLocation(coords[index][0], coords[index - 1][1]);
                                line[1].setLocation(coords[index - 1][0], coords[index - 1][1]);
                                editTime = false;
                                currentEvent = index - 1;
			}
		return line;
	}
	
	/*
	* 	This funktion converts points (pixels) to beats per minute
	* 	@param delta
	* 	@returns bpm
	*/

	private int pointsToBpm(int delta) {
		return (maxTempo*((-1*delta)+height))/height; 
	}

	private boolean isEditing = false;
	private Point[] currentEditLine;
	private Point p,p2;
	private int currentEvent;
	private boolean editTime = true;
	
	private class TempoMouseListener extends MouseAdapter {
		
		public void mousePressed(MouseEvent e) {
			p2 = p = e.getPoint();
			switch( App.UI.getTool() ){
                                case POINTER: 
					currentEditLine = findClosestLine(p);
                                	isEditing = true;
					break;
				case ERASER:
					coords = recalculateCoords();
					currentEditLine = findClosestLine(p);
					App.Project.removeTempoChange(currentEvent); 
					break;
                                case PENCIL:
					if (tc.size() == 0) {
						App.Project.newTempoChange(500000,0);
						App.Project.newTempoChange(500000,GridHelper.xPositionToTick(p.x)); 
					} else {
						int index = 0;
                        			while(p.x > coords[index][0] && index < coords.length - 1)
                                			index++;
						App.Project.newTempoChange(bpmToCPQN(bpms[index - 1]),GridHelper.xPositionToTick(p.x));
					}
					coords = recalculateCoords();
					currentEditLine = findClosestLine(p);	
					break;
                        }
			repaint();
		}
		
		public void mouseReleased(MouseEvent e){
			if (isEditing && editTime) {
				App.Project.editTempoChangeTime(currentEvent,GridHelper.xPositionToTick(currentEditLine[0].x));
			} else if (isEditing) {
				if (tc.size() == 0)
					App.Project.newTempoChange(500000,0);
				App.Project.editTempoChangeTempo(currentEvent,60000000/(double)pointsToBpm(currentEditLine[0].y));
				coords = recalculateCoords();
				currentEditLine = findClosestLine(e.getPoint());
			}
			isEditing = false;
			repaint();
		}	
	}
	
	private class TempoMouseMotionListener extends MouseMotionAdapter {
		
		public void mouseDragged(MouseEvent e) {
			if (isEditing)
				if (editTime  // if the line we are editing represents time and we are restrictive to move the line between the lines surrounding this one
				 && coords[currentEvent][0] + (e.getPoint().x - p.x) > coords[currentEvent - 1][0]  
				 && coords[currentEvent][0] + (e.getPoint().x - p.x) < coords[currentEvent + 1][0])
				{
					currentEditLine[0].translate(e.getPoint().x - p.x, 0);
					currentEditLine[1].translate(e.getPoint().x - p.x, 0);
					coords[currentEvent][0] = currentEditLine[0].x;
					p2.translate(e.getPoint().x - p.x,0);
				} else if (coords[currentEvent][1] + (e.getPoint().y - p.y) < height) { // if the line represents tempo, and we don't try to set it to less than zero
					currentEditLine[0].translate(0,e.getPoint().y - p.y);
					currentEditLine[1].translate(0,e.getPoint().y - p.y);
					coords[currentEvent][1] = currentEditLine[0].y;
					p2.translate(0,e.getPoint().y - p.y);
				}
			repaint();
		}
	}
}

